﻿using System;

using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Silvermoon.OpenGL.Native;
using System.Diagnostics;

namespace Silvermoon.OpenGL
{
    /// <summary>
    /// Renders strings using <see cref="FontSprite"/>. Instead of creating a texture for each string, only one texture (from FontSprite) is used
    /// as reference and vertex and texture pointers are used to render the string. TextureString also caches previousely created vertex/texture pointers as long
    /// as any pending property (e.g. <paramref name="Text"/>) does not change.
    /// </summary>
    public class TextSprite : IDisposable
    {
        #region ctor/dtor

        public TextSprite(FontSprite font)
            : this()
        {
            this.Font = font;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TextSprite"/> class.
        /// </summary>
        public TextSprite()
            : base()
        {
            EnableGlesBuffer = false;
        }

        public virtual void Dispose()
        {
            DisposeBuffers();
            GC.SuppressFinalize(this);
        }

        ~TextSprite()
        {
            Dispose();
        }

        #endregion
        #region fields

        float scaleFactor = 1F;

        private Size clipSize;
        private Size textSize;
        private FontSprite font;
        private int characterSpace;
        private string text = string.Empty;
        private short[] indexPtr;
        private int indexPtrLength;
        private short[] vertexPtr;
        private float[] fontPtr;
        private uint indexPtrName;
        private uint vertexPtrName;
        private uint fontPtrName;
        private int vertexPtrLength;
        private int fontPtrLength;
        private bool invalidated = true;
        private LineMode mode = LineMode.SingleLine;
        private Alignment alignment = Alignment.Center;
        private Alignment lineAlignment = Alignment.Center;

        #endregion
        #region Props


        /// <summary>
        /// Gets or sets whether GLES buffering for vertex, color and index pointers is enabled (default=true).
        /// </summary>
        public bool EnableGlesBuffer
        {
            get;
            set;
        }

        public bool WordWrap
        {
            get
            {
                return (mode & LineMode.WordWrap) == LineMode.WordWrap;
            }
        }

        public bool SingleLine
        {
            get
            {
                return (mode & LineMode.WordWrap) == LineMode.SingleLine;
            }
        }

        public bool MultiLine
        {
            get
            {
                return (mode & LineMode.WordWrap) == LineMode.MultiLine;
            }
        }

        public bool EndEllipsis
        {
            get
            {
                return (mode & LineMode.EndEllipsis) != 0;
            }
        }

        public Alignment Alignment
        {
            get
            {
                return alignment;
            }
            set
            {
                if (alignment != value)
                {
                    alignment = value;
                    InvalidateData();
                }
            }
        }

        public Alignment LineAlignment
        {
            get
            {
                return lineAlignment;
            }
            set
            {
                if (lineAlignment != value)
                {
                    lineAlignment = value;
                    InvalidateData();
                }
            }
        }


        /// <summary>
        /// Gets or sets how to handle multiple lines.
        /// </summary>
        public LineMode LineMode
        {
            get
            {
                return mode;
            }
            set
            {
                if (value == LineMode.EndEllipsis) value = LineMode.EndEllipsis | LineMode.SingleLine;
                if (mode != value)
                {
                    mode = value;
                    InvalidateData();
                }
            }
        }

        /// <summary>
        /// Gets or sets the factor how text is scaled. If ScaleFactor>1f, text gets larger, otherwise smaller.
        /// This enables TextSprite to use only one (large) TextFont to render text in different sizes instead of using different TextFont for each size with different underlying textures and
        /// thus saving graphic resources.
        /// </summary>
        public float ScaleFactor
        {
            get
            {
                return scaleFactor;
            }
            set
            {
                if (scaleFactor != value)
                {
                    scaleFactor = value;
                }
            }
        }


        /// <summary>
        /// Gets or sets the <see cref="FontSpite"/> to use as reference to render each character refered to the specified <see cref="FontSprite"/>.
        /// </summary>
        public FontSprite Font
        {
            get
            {
                return font ?? FontSprite.Regular;
            }
            set
            {
                if (font != value)
                {
                    font = value;
                    InvalidateData();
                }
            }
        }

        /// <summary>
        /// Gets or sets the text to render.
        /// </summary>
        public string Text
        {
            get
            {
                return text ?? String.Empty;
            }
            set
            {
                if (text != value)
                {
                    text = value;
                    InvalidateData();
                }
            }
        }

        /// <summary>
        /// Gets the actual size of the string.
        /// </summary>
        public Size TextSize
        {
            get
            {
                if (textSize.IsEmpty) textSize = MeasureSize();
                return textSize;
            }
        }

        /// <summary>
        /// Gets or sets the maximum allowed size for the text. if set to 0/0, this value is ignored.
        /// </summary>
        public Size ClipSize
        {
            get
            {
                return clipSize;
            }
            set
            {
                if (clipSize != value)
                {
                    clipSize = value;
                    InvalidateData();
                }
            }
        }

        public Color Color
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the space between characters.
        /// </summary>
        public int CharacterSpace
        {
            get
            {
                return characterSpace;
            }
            set
            {
                if (characterSpace != value)
                {
                    characterSpace = value;
                    InvalidateData();
                }
            }
        }

        #endregion
        #region Methods

        private void DisposeBuffers()
        {
            if (indexPtrName > 0)
            {
                gl.DeleteBuffer(indexPtrName);
                indexPtrName = 0;
            }
            if (vertexPtrName > 0)
            {
                gl.DeleteBuffer(vertexPtrName);
                vertexPtrName = 0;
            }
            if (fontPtrName > 0)
            {
                gl.DeleteBuffer(fontPtrName);
                fontPtrName = 0;
            }
            invalidated = true;
        }

        /// <summary>
        /// Invalidates the calculated data
        /// </summary>
        private void InvalidateData()
        {
            invalidated = true;
            textSize = Size.Empty;
            if (Invalidated != null) Invalidated(this, EventArgs.Empty);
        }

        public event EventHandler Invalidated;

        private Size fontTextureSize;

        /// <summary>
        /// Calculates the arrays for the text.
        /// </summary>
        private void CalculateData()
        {
            fontTextureSize = Font.Texture.StorageSize;
            IList<string> lines = GetLines(mode, text, true);
            int n = SumOfChars(lines);

            if (n == 0)
            {
                indexPtr = vertexPtr = null;
                fontPtr = null;
                fontPtrLength = vertexPtrLength = indexPtrLength = 0;
                invalidated = false;
                return;
            }

            vertexPtrLength = n * 2 * 4;
            indexPtrLength = n * 6;
            fontPtrLength = vertexPtrLength;


            indexPtr = new short[indexPtrLength];
            vertexPtr = new short[vertexPtrLength];
            fontPtr = new float[fontPtrLength];

            int index = 0;
            short y = 0;

            if (clipSize.Height > 0)
            {
                switch (lineAlignment)
                {
                    case Alignment.Center:
                        y = (short)((clipSize.Height - (lines.Count * Font.CharHeight * scaleFactor)) / 2);
                        break;

                    case Alignment.Far:
                        y = (short)(clipSize.Height - (lines.Count * Font.CharHeight * scaleFactor));
                        break;
                }
            }

            short charHeight = (short)(Font.CharHeight * scaleFactor);
            foreach (string line in lines)
            {
                index = CalculateLine(line, line.Length, 0, y, index);
                y += charHeight;
            }

            if (EnableGlesBuffer) CreateBuffers();

            invalidated = false;
        }

        private void CreateBuffers()
        {
            if (indexPtrName == 0) indexPtrName = gl.GenBuffer();
            Debug.WriteLine("GenBuffer #" + indexPtrName.ToString());

            if (indexPtrName > 0)
            {
                gl.BindBuffer(BufferTarget.ElementArrayBuffer, indexPtrName);
                gl.CheckError();
                gl.BufferData(BufferTarget.ElementArrayBuffer, indexPtrLength * sizeof(short), indexPtr, BufferUsage.StaticDraw);
                gl.CheckError();
                gl.BindBuffer(BufferTarget.ElementArrayBuffer, 0);
                gl.CheckError();
                indexPtr = null;
            }

            if (vertexPtrName == 0) vertexPtrName = gl.GenBuffer();
            gl.CheckError();
            if (vertexPtrName > 0)
            {
                gl.BindBuffer(BufferTarget.ArrayBuffer, vertexPtrName);
                gl.CheckError();
                gl.BufferData(BufferTarget.ArrayBuffer, vertexPtrLength * sizeof(short), vertexPtr, BufferUsage.StaticDraw);
                gl.CheckError();
                gl.BindBuffer(BufferTarget.ArrayBuffer, 0);
                gl.CheckError();
                vertexPtr = null;
            }

            if (fontPtrName == 0) fontPtrName = gl.GenBuffer();
            if (fontPtrName > 0)
            {
                gl.BindBuffer(BufferTarget.ArrayBuffer, fontPtrName);
                gl.CheckError();
                gl.BufferData(BufferTarget.ArrayBuffer, fontPtrLength * sizeof(float), fontPtr, BufferUsage.StaticDraw);
                gl.CheckError();
                gl.BindBuffer(BufferTarget.ArrayBuffer, 0);
                gl.CheckError();
                fontPtr = null;
            }
        }

        private int SumOfChars(IEnumerable<string> lines)
        {
            int n = 0;
            foreach (var s in lines)
            {
                n += s.Length;
            }
            return n;
        }

        /// <summary>
        /// Gets all lines for the text.
        /// </summary>
        /// <returns></returns>
        public IList<string> GetLines(LineMode mode, string text, bool clip)
        {
            Size clipSize = this.clipSize;
            clipSize.Width = (int)(clipSize.Width / scaleFactor);
            clipSize.Height = (int)(clipSize.Height / scaleFactor);

            switch (mode & LineMode.WordWrap)
            {
                case LineMode.SingleLine:
                    return GetSingleLine(text, clipSize);

                case LineMode.MultiLine:
                    return Font.GetLines(text, clipSize, EndEllipsis, characterSpace);

                case LineMode.WordWrap:
                    return Font.WordWrapLines(text, clipSize, characterSpace);

                default:
                    throw new NotSupportedException(mode.ToString());
            }
        }

        private IList<string> GetSingleLine(string text, Size clipSize)
        {
            string line = text;
            int w = Font.MeasureStringInternal(ref line, clipSize.Width, characterSpace, EndEllipsis);
            return new string[] { line };
        }

        private int CalculateLine(string text, int n, short x, short y, int index)
        {
            short h = (short)(Font.CharHeight * scaleFactor);
            short charSpace = (short)CharacterSpace;
            if (text != null)
            {
                int start = index;
                foreach (char c in text)
                {
                    if (--n < 0) break;
                    short charWidth = (short)(Font.CharacterWidth(c) * scaleFactor);

                    AddCharacterArrayEntry(index, x, y, (short)(x + charWidth), (short)(y + h));
                    AddIndexEntry(index);
                    CopyFontEntry(index, c);

                    index++;
                    x += (short)(charWidth + charSpace);
                }
                int w = clipSize.Width;
                if (alignment != Alignment.Near && w > 0)
                {
                    short delta = alignment == Alignment.Far ? (short)(w - x) : (short)((w - x) / 2);
                    AddAlignment(start, index, delta);
                }
            }
            return index;
        }

        private void AddAlignment(int start, int index, short offset)
        {
            short[] a = vertexPtr;
            for (int i = start; i < index; i++)
            {
                int aIndex = i * 8;

                a[aIndex + 0] += offset;
                a[aIndex + 2] += offset;
                a[aIndex + 4] += offset;
                a[aIndex + 6] += offset;
            }
        }

        private int GetMaxChars(string text)
        {
            return GetMaxChars(text, clipSize.Width);
        }

        private int GetMaxChars(string text, int width)
        {
            if (string.IsNullOrEmpty(text)) return 0;

            int max = (int)(width / scaleFactor);
            if (max == 0f) return text.Length;
            int n = 0;
            int w = -1; // must not be null, otherwhise a MeasureString would return a width that does NOT fit in all the charactors!
            foreach (char c in text)
            {
                int charWidth = Font.CharacterWidth(c);
                w += charWidth;
                if (w > max) break;
                n++;
            }
            int l = text.Length;
            return n < l ? n : l;
        }

        private unsafe void AddIndexEntry(int index)
        {
            int stripIndex = (int)(index * 4);
            int ixPos = index * 6;
            fixed (short* ix = &indexPtr[ixPos])
            {
                *(ix + 0) = (short)stripIndex;
                *(ix + 1) = (short)(stripIndex + 1);
                *(ix + 2) = (short)(stripIndex + 2);
                *(ix + 3) = (short)(stripIndex + 2);
                *(ix + 4) = (short)(stripIndex + 1);
                *(ix + 5) = (short)(stripIndex + 3);
            }
        }

        private unsafe void CopyFontEntry(int pos, char c)
        {
            var charData = Font.GetCharData(c);
            if (charData == null) return;
            pos *= 8;

            int x = charData.X;
            int y = charData.Y;
            int cw = charData.Width;

            int ch = Font.CharHeight;

            float w = fontTextureSize.Width;
            float h = fontTextureSize.Height - 1;

            float left = (float)x / w;
            float top = (float)y / h;
            float right = (float)(x + cw) / w;
            float bottom = (float)(y + ch) / h;

            fixed (float* a = &fontPtr[pos])
            {
                *a = left;
                *(a + 1) = bottom;
                *(a + 2) = left;
                *(a + 3) = top;
                *(a + 4) = right;
                *(a + 5) = bottom;
                *(a + 6) = right;
                *(a + 7) = top;
            }
        }

        /// <summary>
        /// Gets the right bottom point for a character.
        /// This is a very fast method since it does not require any analysis but receives the data from precalculated values.
        /// </summary>
        /// <param name="charIndex">Index of the character.</param>
        /// <returns>Point with the right bottom point for the character when rendered.</returns>
        public Point GetPositionAt(int charIndex)
        {
            if (vertexPtr == null) return Point.Empty;
            int max = vertexPtr != null ? vertexPtr.Length / 8 : 0;
            if (charIndex >= max) charIndex = max - 1;

            int ix = charIndex * 8;
            int right = vertexPtr[ix + 4];
            int bottom = vertexPtr[ix + 1];
            return new Point(right, bottom);
        }

        /// <summary>
        /// Gets the dimensions for a character.
        /// This is a very fast method since it does not require any analysis but receives the data from precalculated values.
        /// </summary>
        /// <param name="charIndex">Index of the character.</param>
        /// <returns>Rectangle with the bounds of the character when rendered.</returns>
        public Rectangle GetDimension(int charIndex)
        {
            if (vertexPtr == null) return Rectangle.Empty;
            int max = vertexPtr != null ? vertexPtr.Length / 8 : 0;
            if (charIndex >= max) charIndex = max - 1;
            int ix = charIndex * 8;
            int left = vertexPtr[ix];
            int top = vertexPtr[ix + 3];
            int right = vertexPtr[ix + 4];
            int bottom = vertexPtr[ix + 1];
            return new Rectangle(left, top, right - left, bottom - top);
        }



        private unsafe void AddCharacterArrayEntry(int index, short left, short top, short right, short bottom)
        {
            int aIndex = index * 8;

            fixed (short* a = &vertexPtr[aIndex])
            {
                *(a + 0) = left;
                *(a + 1) = bottom;
                *(a + 2) = left;
                *(a + 3) = top;
                *(a + 4) = right;
                *(a + 5) = bottom;
                *(a + 6) = right;
                *(a + 7) = top;
            }
        }

        private Size MeasureSize()
        {
            IList<string> lines = GetLines(mode, text, true);
            int h = lines.Count * Font.CharHeight;
            int width = 0;
            foreach (string line in lines)
            {
                int w = Font.MeasureString(line, clipSize.Width, characterSpace, false);
                if (width < w) width = w;
            }
            return new Size((int)(width * scaleFactor + 0.5f), (int)(h * scaleFactor + 0.5f));
        }



        public void PreLoad()
        {
            if (invalidated) CalculateData();
        }

        /// <summary>
        /// Renders the <paramref name="Text"/> property at the current matrix view location.
        /// </summary>
        public void Render()
        {
            if (fontPtrLength == 0) return;
            PreLoad();

            if (!Color.IsEmpty)
            {
                gl.Color4x(Color.R << 8, Color.G << 8, Color.B << 8, Color.A << 8);
                gl.CheckError();
            }
            gl.BindTexture(TextureTarget.Texture2D, Font.Texture.Name);
            gl.CheckError();

            if (fontPtr != null) gl.TexCoordPointer(2, glValueType.Float, 0, this.fontPtr);
            else if (fontPtrName > 0)
            {
                gl.BindBuffer(BufferTarget.ArrayBuffer, fontPtrName);
                gl.CheckError();
                gl.TexCoordPointer(2, glValueType.Float, 0, (float[])null);
                gl.CheckError();
                gl.BindBuffer(BufferTarget.ArrayBuffer, 0);
                gl.CheckError();
            }
            gl.CheckError();

            if (vertexPtr != null) gl.VertexPointer(2, glValueType.Short, 0, vertexPtr);
            else if (indexPtrName > 0)
            {
                gl.BindBuffer(BufferTarget.ArrayBuffer, vertexPtrName);
                gl.CheckError();
                gl.VertexPointer(2, glValueType.Short, 0, (short[])null);
                gl.CheckError();
                gl.BindBuffer(BufferTarget.ArrayBuffer, 0);
                gl.CheckError();
            }


            if (indexPtr != null) gl.DrawElements(PrimitiveType.Triangles, indexPtrLength, glValueType.UnsignedShort, indexPtr);
            else if (indexPtrName > 0)
            {
                gl.BindBuffer(BufferTarget.ElementArrayBuffer, indexPtrName);
                gl.CheckError();
                gl.DrawElements(PrimitiveType.Triangles, indexPtrLength, glValueType.UnsignedShort, (short[])null);
                gl.CheckError();
                gl.BindBuffer(BufferTarget.ElementArrayBuffer, 0);
                gl.CheckError();
            }

        }

        /// <summary>
        /// Renders the specified string and sets this string as current <paramref name="Text"/> property. 
        /// After rendering, the current matrix view is transfered by one character row.
        /// </summary>
        /// <param name="text">The Text to render.</param>
        public void Render(string text)
        {
            Text = text;
            Render();
            gl.Translatef(0f, TextSize.Height, 0f);
            gl.CheckError();
        }

        #endregion
    }
}
